/**
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.apache.solr.update;

import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.List;

import org.apache.lucene.document.Document;
import org.apache.lucene.document.Fieldable;
import org.apache.solr.common.SolrDocument;
import org.apache.solr.common.SolrException;
import org.apache.solr.common.SolrInputDocument;
import org.apache.solr.common.SolrInputField;
import org.apache.solr.schema.*;

/**
 * @version $Id: DocumentBuilder.java 1304001 2012-03-22 19:33:14Z hossman $
 */

// Not thread safe - by design. Create a new builder for each thread.
public class DocumentBuilder {
	private final IndexSchema schema;
	private Document doc;
	private HashMap<String, String> map;

	public DocumentBuilder(IndexSchema schema) {
		this.schema = schema;
	}

	public void startDoc() {
		doc = new Document();
		map = new HashMap<String, String>();
	}

	protected void addSingleField(SchemaField sfield, String val, float boost) {
		// System.out.println("###################ADDING FIELD "+sfield+"="+val);

		// we don't check for a null val ourselves because a solr.FieldType
		// might actually want to map it to something. If createField()
		// returns null, then we don't store the field.
		if (sfield.isPolyField()) {
			Fieldable[] fields = sfield.createFields(val, boost);
			if (fields.length > 0) {
				if (!sfield.multiValued()) {
					String oldValue = map.put(sfield.getName(), val);
					if (oldValue != null) {
						throw new SolrException(
								SolrException.ErrorCode.BAD_REQUEST,
								"ERROR: multiple values encountered for non multiValued field "
										+ sfield.getName() + ": first='"
										+ oldValue + "' second='" + val + "'");
					}
				}
				// Add each field
				for (Fieldable field : fields) {
					doc.add(field);
				}
			}
		} else {
			Fieldable field = sfield.createField(val, boost);
			if (field != null) {
				if (!sfield.multiValued()) {
					String oldValue = map.put(sfield.getName(), val);
					if (oldValue != null) {
						throw new SolrException(
								SolrException.ErrorCode.BAD_REQUEST,
								"ERROR: multiple values encountered for non multiValued field "
										+ sfield.getName() + ": first='"
										+ oldValue + "' second='" + val + "'");
					}
				}
			}
			doc.add(field);
		}

	}

	/**
	 * Add the specified {@link org.apache.solr.schema.SchemaField} to the
	 * document. Does not invoke the copyField mechanism.
	 * 
	 * @param sfield
	 *            The {@link org.apache.solr.schema.SchemaField} to add
	 * @param val
	 *            The value to add
	 * @param boost
	 *            The boost factor
	 * 
	 * @see #addField(String, String)
	 * @see #addField(String, String, float)
	 * @see #addSingleField(org.apache.solr.schema.SchemaField, String, float)
	 */
	public void addField(SchemaField sfield, String val, float boost) {
		addSingleField(sfield, val, boost);
	}

	/**
	 * Add the Field and value to the document, invoking the copyField mechanism
	 * 
	 * @param name
	 *            The name of the field
	 * @param val
	 *            The value to add
	 * 
	 * @see #addField(String, String, float)
	 * @see #addField(org.apache.solr.schema.SchemaField, String, float)
	 * @see #addSingleField(org.apache.solr.schema.SchemaField, String, float)
	 */
	public void addField(String name, String val) {
		addField(name, val, 1.0f);
	}

	/**
	 * Add the Field and value to the document with the specified boost,
	 * invoking the copyField mechanism
	 * 
	 * @param name
	 *            The name of the field.
	 * @param val
	 *            The value to add
	 * @param boost
	 *            The boost
	 * 
	 * @see #addField(String, String)
	 * @see #addField(org.apache.solr.schema.SchemaField, String, float)
	 * @see #addSingleField(org.apache.solr.schema.SchemaField, String, float)
	 * 
	 */
	public void addField(String name, String val, float boost) {
		SchemaField sfield = schema.getFieldOrNull(name);
		if (sfield != null) {
			addField(sfield, val, boost);
		}

		// Check if we should copy this field to any other fields.
		// This could happen whether it is explicit or not.
		final List<CopyField> copyFields = schema.getCopyFieldsList(name);
		if (copyFields != null) {
			for (CopyField cf : copyFields) {
				addSingleField(cf.getDestination(), cf.getLimitedValue(val),
						boost);
			}
		}

		// error if this field name doesn't match anything
		if (sfield == null && (copyFields == null || copyFields.size() == 0)) {
			throw new SolrException(SolrException.ErrorCode.BAD_REQUEST,
					"ERROR:unknown field '" + name + "'");
		}
	}

	public void setBoost(float boost) {
		doc.setBoost(boost);
	}

	public void endDoc() {
	}

	// specific to this type of document builder
	public Document getDoc() throws IllegalArgumentException {

		// Check for all required fields -- Note, all fields with a
		// default value are defacto 'required' fields.
		List<String> missingFields = null;
		for (SchemaField field : schema.getRequiredFields()) {
			if (doc.getFieldable(field.getName()) == null) {
				if (field.getDefaultValue() != null) {
					addField(doc, field, field.getDefaultValue(), 1.0f);
				} else {
					if (missingFields == null) {
						missingFields = new ArrayList<String>(1);
					}
					missingFields.add(field.getName());
				}
			}
		}

		if (missingFields != null) {
			StringBuilder builder = new StringBuilder();
			// add the uniqueKey if possible
			if (schema.getUniqueKeyField() != null) {
				String n = schema.getUniqueKeyField().getName();
				String v = doc.get(n);
				builder.append("Document [" + n + "=" + v + "] ");
			}
			builder.append("missing required fields: ");
			for (String field : missingFields) {
				builder.append(field);
				builder.append(" ");
			}
			throw new SolrException(SolrException.ErrorCode.BAD_REQUEST,
					builder.toString());
		}

		Document ret = doc;
		doc = null;
		return ret;
	}

	private static void addField(Document doc, SchemaField field, String val,
			float boost) {
		if (field.isPolyField()) {
			Fieldable[] farr = field.getType().createFields(field, val, boost);
			for (Fieldable f : farr) {
				if (f != null)
					doc.add(f); // null fields are not added
			}
		} else {
			Fieldable f = field.createField(val, boost);
			if (f != null)
				doc.add(f); // null fields are not added
		}
	}

	private static String getID(SolrInputDocument doc, IndexSchema schema) {
		String id = "";
		SchemaField sf = schema.getUniqueKeyField();
		if (sf != null) {
			id = "[doc=" + doc.getFieldValue(sf.getName()) + "] ";
		}
		return id;
	}

	/**
	 * Convert a SolrInputDocument to a lucene Document.
	 * 
	 * This function should go elsewhere. This builds the Document without an
	 * extra Map<> checking for multiple values. For more discussion, see:
	 * http:/
	 * /www.nabble.com/Re%3A-svn-commit%3A-r547493---in--lucene-solr-trunk%
	 * 3A-.--
	 * src-java-org-apache-solr-common--src-java-org-apache-solr-schema--src
	 * -java
	 * -org-apache-solr-update--src-test-org-apache-solr-common--tf3931539.html
	 * 
	 * TODO: /!\ NOTE /!\ This semantics of this function are still in flux.
	 * Something somewhere needs to be able to fill up a SolrDocument from a
	 * lucene document - this is one place that may happen. It may also be moved
	 * to an independent function
	 * 
	 * @since solr 1.3
	 */
	public static Document toDocument(SolrInputDocument doc, IndexSchema schema) {
		Document out = new Document();
		out.setBoost(doc.getDocumentBoost());

		// Load fields from SolrDocument to Document
		for (SolrInputField field : doc) {
			String name = field.getName();
			SchemaField sfield = schema.getFieldOrNull(name);
			boolean used = false;
			float boost = field.getBoost();

			// Make sure it has the correct number
			if (sfield != null && !sfield.multiValued()
					&& field.getValueCount() > 1) {
				throw new SolrException(
						SolrException.ErrorCode.BAD_REQUEST,
						"ERROR: "
								+ getID(doc, schema)
								+ "multiple values encountered for non multiValued field "
								+ sfield.getName() + ": " + field.getValue());
			}

			if (sfield != null && sfield.omitNorms() && boost != 1.0F) {
				throw new SolrException(
						SolrException.ErrorCode.BAD_REQUEST,
						"ERROR: "
								+ getID(doc, schema)
								+ "cannot set an index-time boost, norms are omitted for field "
								+ sfield.getName() + ": " + field.getValue());
			}

			// load each field value
			boolean hasField = false;
			try {
				for (Object v : field) {
					if (v == null) {
						continue;
					}
					String val = null;
					hasField = true;
					boolean isBinaryField = false;
					if (sfield != null
							&& sfield.getType() instanceof BinaryField) {
						isBinaryField = true;
						BinaryField binaryField = (BinaryField) sfield
								.getType();
						Fieldable f = binaryField.createField(sfield, v, boost);
						if (f != null) {
							out.add(f);
						}
						used = true;
					} else {
						// TODO!!! HACK -- date conversion
						if (sfield != null && v instanceof Date
								&& sfield.getType() instanceof DateField) {
							DateField df = (DateField) sfield.getType();
							val = df.toInternal((Date) v) + 'Z';
						} else if (v != null) {
							val = v.toString();
						}

						if (sfield != null) {
							used = true;
							addField(out, sfield, val, boost);
						}
					}

					// Check if we should copy this field to any other fields.
					// This could happen whether it is explicit or not.
					List<CopyField> copyFields = schema.getCopyFieldsList(name);
					for (CopyField cf : copyFields) {
						SchemaField destinationField = cf.getDestination();
						// check if the copy field is a multivalued or not
						if (!destinationField.multiValued()
								&& out.get(destinationField.getName()) != null) {
							throw new SolrException(
									SolrException.ErrorCode.BAD_REQUEST,
									"ERROR: "
											+ getID(doc, schema)
											+ "multiple values encountered for non multiValued copy field "
											+ destinationField.getName() + ": "
											+ val);
						}

						used = true;
						// Don't worry about poly fields here
						Fieldable[] fields = null;
						if (isBinaryField) {
							if (destinationField.getType() instanceof BinaryField) {
								BinaryField binaryField = (BinaryField) destinationField
										.getType();
								// TODO: safe to assume that binary fields only
								// create one?
								fields = new Fieldable[] { binaryField
										.createField(destinationField, v, boost) };
							}
						} else {
							fields = destinationField.createFields(
									cf.getLimitedValue(val), boost);
						}
						if (fields != null) { // null fields are not added
							for (Fieldable f : fields) {
								if (f.getBoost() != 1.0F && f.getOmitNorms()) {
									// the field wasnt explicitly boosted, null
									// out the boost
									f.setBoost(1.0F);
								}
								out.add(f);
							}
						}
					}

					// In lucene, the boost for a given field is the product of
					// the
					// document boost and *all* boosts on values of that field.
					// For multi-valued fields, we only want to set the boost on
					// the
					// first field.
					boost = 1.0f;
				}
			} catch (Exception ex) {
				throw new SolrException(SolrException.ErrorCode.BAD_REQUEST,
						"ERROR: " + getID(doc, schema) + "Error adding field '"
								+ field.getName() + "'='" + field.getValue()
								+ "'", ex);
			}

			// make sure the field was used somehow...
			if (!used && hasField) {
				throw new SolrException(SolrException.ErrorCode.BAD_REQUEST,
						"ERROR: " + getID(doc, schema) + "unknown field '"
								+ name + "'");
			}
		}

		// Now validate required fields or add default values
		// fields with default values are defacto 'required'
		for (SchemaField field : schema.getRequiredFields()) {
			if (out.getFieldable(field.getName()) == null) {
				if (field.getDefaultValue() != null) {
					addField(out, field, field.getDefaultValue(), 1.0f);
				} else {
					String msg = getID(doc, schema)
							+ "missing required field: " + field.getName();
					throw new SolrException(
							SolrException.ErrorCode.BAD_REQUEST, msg);
				}
			}
		}
		return out;
	}

	/**
	 * Deprecated, do not use: The semantics of this method make no sense, and
	 * have never made sense, this method will be removed in Solr 4.0
	 * 
	 * @deprecated Do Not Use, see SolrPluginUtils.docListToSolrDocumentList
	 *             instead
	 * @see org.apache.solr.util.SolrPluginUtils#docListToSolrDocumentList
	 */
	@Deprecated
	public SolrDocument loadStoredFields(SolrDocument doc, Document luceneDoc) {
		for (Object f : luceneDoc.getFields()) {
			Fieldable field = (Fieldable) f;
			if (field.isStored()) {
				SchemaField sf = schema.getField(field.name());
				if (!schema.isCopyFieldTarget(sf)) {
					doc.addField(field.name(), sf.getType().toObject(field));
				}
			}
		}
		return doc;
	}
}
